/*
 *  linux/init/main.c
 *
 *  (C) 1991  Linus Torvalds
 */

#define __LIBRARY__
#include <unistd.h>
#include <time.h>

/*
 * 我们需要下面这些内嵌语句- 从内核空间创建进程(forking)将导致没有写时复
 * 制（COPY ON WRITE）!!!直到一个执行execve 调用。这对堆栈可能带来问题。处
 * 理的方法是在fork()调用之后不让main()使用任何堆栈。因此就不能有函数调
 * 用- 这意味着fork 也要使用内嵌的代码，否则我们在从fork()退出时就要使用堆栈了。
 *
 * 实际上只有pause 和fork 需要使用内嵌方式，以保证从main()中不会弄乱堆栈，
 * 但是我们同时还定义了其它一些函数。
 */
inline fork(void) __attribute__((always_inline));
inline pause(void) __attribute__((always_inline));
inline _syscall0(int,fork)
inline _syscall0(int,pause)
_syscall1(int,setup,void *,BIOS)
inline _syscall0(int,sync)

#include <linux/tty.h>
#include <linux/sched.h>
#include <linux/head.h>
#include <asm/system.h>
#include <asm/io.h>

#include <stddef.h>
#include <stdarg.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>

#include <linux/fs.h>

static char printbuf[1024];			// 用于下面的Printf打印

extern int vsprintf();
extern void init(void);
extern void blk_dev_init(void);
extern void chr_dev_init(void);
extern void hd_init(void);
extern void floppy_init(void);
extern void mem_init(long start, long end);
extern long rd_init(long mem_start, int length);
extern long kernel_mktime(struct tm * tm);
extern long startup_time;

/*
 * 以下这些数据是由setup.s 程序在引导时间设置的。
 */
#define EXT_MEM_K (*(unsigned short *)0x90002)
#define DRIVE_INFO (*(struct drive_info *)0x90080)
#define ORIG_ROOT_DEV (*(unsigned short *)0x901FC)

/*
 * 是啊，是啊，下面这段程序很差劲，但我不知道如何正确地实现，而且好象
 * 它还能运行。如果有关于实时时钟更多的资料，那我很感兴趣。这些都是试
 * 探出来的，以及看了一些bios 程序，呵！
 */
// 读版本上的RTC端口，即用电池供电的一个组件，用来保存时间相关的信息
#define CMOS_READ(addr) ({ \
outb_p(0x80|addr,0x70); \
inb_p(0x71); \
})

// 读取出来的编码为BCD码， 转换成二进制
// BCD码表示方法，以45为例，其8421BCD编码为0x45，也就我们读取出来的数
// 是0x45，转换出来的是十进制69，显示不能直接使用。需要转换
// 转换方法就是: 高4位 * 10 + 低4位 = (var>>4)*10 + val & 0xf
#define BCD_TO_BIN(val) ((val)=((val)&15) + ((val)>>4)*10)

// 时间初始化
// 从CMOS实时时钟芯片中读取当前时间，然后转换成1970-1-1 0时的秒数
// 这样就知道了系统当前的时间。这个时间可以用在比如更新文件的读写时间上边
// 以及提供给系统调用API给用户进程使用
static void time_init(void)
{
	struct tm time;

	// 读取当前的时间。读取较慢有可能出错
	// 比如开始读的时间为17-12-31 23:59:59，到最后读取年时，时间可能已经来到了
	// 18-1-1 0:0:0，那么此时读得的时间为18-12-31 23:59:59。差别就很大
	// 因此通过秒来判断整个读取过程中时间是否未发生秒级的改变来保证正确性
	do {
		time.tm_sec = CMOS_READ(0);
		time.tm_min = CMOS_READ(2);
		time.tm_hour = CMOS_READ(4);
		time.tm_mday = CMOS_READ(7);
		time.tm_mon = CMOS_READ(8);
		time.tm_year = CMOS_READ(9);		// 这里年是用1个字节表示
	} while (time.tm_sec != CMOS_READ(0));

	// 转换为BCD码
	BCD_TO_BIN(time.tm_sec);
	BCD_TO_BIN(time.tm_min);
	BCD_TO_BIN(time.tm_hour);
	BCD_TO_BIN(time.tm_mday);
	BCD_TO_BIN(time.tm_mon);
	BCD_TO_BIN(time.tm_year);
	time.tm_mon--;

	// 转换成相对于1970-1-1 0时的秒数，表示系统是从1970-1-1 0时startup_time秒后开机的
	// 这个时间跟Unix延生时间有关，感兴趣可能网上搜索一下
	startup_time = kernel_mktime(&time);		// 在sched.c中定义
}

static long memory_end = 0;
static long buffer_memory_end = 0;
static long main_memory_start = 0;

struct drive_info { char dummy[32]; } drive_info;

// 下面是我加的，原来的linux是没有这些参数
int main(int argc, char ** argv, char ** env) 
//void main(void)		/* This really IS void, no error here. */
{			/* The startup routine assumes (well, ...) this */
/*
 * Interrupts are still disabled. Do necessary setups, then
 * enable them
 */
	// 在进入保护模式前，全局中中断已经关掉，所以下面有关硬件初始化即便
	// 产生中断请求，也不会立即响应
	// 这里从之前保存在内存的硬件信息表中获取相关的参数
 	ROOT_DEV = ORIG_ROOT_DEV;			// 根文件系统设备，由bootsect设置，在0x901FC处
 	drive_info = DRIVE_INFO;			// 由setup设置的hd0硬盘参数表
	//setups.s获取的存储容量大小，EXT_MEM_K为1M以上的内存KB数
	// 所以实际的内存容量应为1MB + EXT_MEM_K * 1024
	memory_end = (1<<20) + (EXT_MEM_K<<10);
	// 分页机制要求内存以4KB页为单位进行管理，所以如果最后不足4KB，则忽略掉
	memory_end &= 0xfffff000;
	// 只支持最多16MB的内存
	if (memory_end > 16*1024*1024)
		memory_end = 16*1024*1024;

	// 调整缓存在内核程序的后边，如果内存容量大，则缓存就设置得大一些，如果小就设置的小一些
	// 缓存用于文件系统的读写
	if (memory_end > 12*1024*1024)
		buffer_memory_end = 4*1024*1024;
	else if (memory_end > 6*1024*1024)
		buffer_memory_end = 2*1024*1024;
	else
		buffer_memory_end = 1*1024*1024;

	// 主内存区，在高速缓存后边。但如果使用虚拟盘，则放置在虚拟盘后边
	main_memory_start = buffer_memory_end;

	// ramdisk，其大小由于Makefile中的GCC参数来给定 -DRAMDISK=512
#ifdef RAMDISK
	main_memory_start += rd_init(main_memory_start, RAMDISK*1024);
#endif
	// 主内存区的初始化，用于按4KB的页进行分配的存储空间
	mem_init(main_memory_start,memory_end);		// 存储初始化，将指定区域用来初始化分配内存
	trap_init();			// 中断和异常初始化, 主要是配置IDT表
	blk_dev_init();			// 块设备初始化，用于磁盘等设备
	chr_dev_init();			// 初符设备初始化, 用于串口和键盘等设备
	tty_init();				// tty初始化，显示器、键盘等tty显示
	time_init();			// 时间初始化，从RTC中获取当前的系统时间
	sched_init();			// 调度器初始化，涉及多进程的处理
	buffer_init(buffer_memory_end);		// 高速缓存初始化，主要初始化一些结构
	hd_init();				// 硬盘初始化，主要是配置中断和一些请求
	floppy_init();			// 软盘初始化,同磁盘的初始化

	// 所有初始化完成后，包括中断处理程序设置好后，就可以打开全局中中断响应处理了
	sti();		// sti指令的简单封装

	// 系统启动后，工作在特权级0模式下。这个Main的运行相当于是一个进程
	// 这里将当前main程序切换成特权级3模式，然后作为首个进程运行
	move_to_user_mode();

	// fork == 0时执行init，即创建出的新进程来执行init函数
	if (!fork()) {		/* we count on this going ok */
		// 创建的子进程，执行初始化函数
		init();
	}
/*
 *   NOTE!!   For any other task 'pause()' would mean we have to get a
 * signal to awaken, but task0 is the sole exception (see 'schedule()')
 * as task 0 gets activated at every idle moment (when no other tasks
 * can run). For task0 'pause()' just means we go check if some other
 * task can run, and if not we return here.
 */
	
	// main对应的首个进程，执行Pause。这个进程为空闲进程，即当系统中所有其它进程
	// 都不运行时，CPU总要干点什么，于是就运行到这里
	for(;;) {
	// ---------- 测试代码 ---------------
		// 异常代码测试
		//int a = 3 / 0; 		// 触发除0异常，没有错误码，以便了解进程运行状态
		//__asm__ __volatile__("int3");
		// __asm__ __volatile__("mov $120, %ax; mov %ax, %ds");		// 触发错误码的异常, 段异常

		// *(unsigned char *)0xffffffff = 0;		// 页处理，触发有错误码的异常
	// ---------- 测试代码 ---------------
		pause();
	}
}

// 仅限本文件使用的Printf函数
static int printf(const char *fmt, ...)
// 产生格式化信息并输出到标准输出设备stdout(1)，这里是指屏幕上显示。参数'*fmt'
// 指定输出将采用的格式，参见各种标准C 语言书籍。该子程序正好是vsprintf 如何使
// 用的一个例子。
// 该程序使用vsprintf()将格式化的字符串放入printbuf 缓冲区，然后用write()
// 将缓冲区的内容输出到标准设备（1--stdout）。
{
	va_list args;
	int i;

	va_start(args, fmt);
	// 格式化到printbuf，然后write输出
	write(1,printbuf,i=vsprintf(printbuf, fmt, args));	
	va_end(args);
	return i;
}

static char * argv_rc[] = { "/bin/sh", NULL };// 调用执行程序时参数的字符串数组。
static char * envp_rc[] = { "HOME=/", NULL };// 调用执行程序时的环境字符串数组。

// sh交互模式下的参数和环境变量
static char * argv[] = { "-/bin/sh",NULL };
static char * envp[] = { "HOME=/usr/root", NULL };

_syscall2(int, kadd, int, a, int, b)

// init是一个普通的应用程序，并没有选择从磁盘上加载
// 而且它位于0地址处的内核中。与其它应用程序不同
void init(void)
{
	int pid,i;
	
	// 挂载根文件系统，接下来才可以进行磁盘的各种操作
	// 比如打开上面的设备文件
	setup((void *) &drive_info);

	// 打开标准输入输出，为后面使用printf做准备
	(void) open("/dev/tty0",O_RDWR,0);			// 标准输入, 可读写，返回值为0
	(void) dup(0);			// 标准输出，返回值为1
	(void) dup(0);			// 标准错误输出，返回值为2

	// --------- 测试代码 ------------------
	int r = kadd(3, 2);
	printf("kadd=%d\r\n", r);
	
	// --------- 测试代码 ------------------


	// 在屏幕上显示相关的信息
	printf("%d buffers = %d bytes buffer space\n\r",NR_BUFFERS, NR_BUFFERS*BLOCK_SIZE);
	printf("Free mem: %d bytes\n\r",memory_end-main_memory_start);
// 下面fork()用于创建一个子进程(子任务)。对于被创建的子进程，fork()将返回0 值，
// 对于原(父进程)将返回子进程的进程号。所以if (!(pid=fork())) {...} 内是子进程执行的内容。
// 该子进程关闭了句柄0(stdin)，以只读方式打开/etc/rc 文件，并执行/bin/sh 程序，所带参数和
// 环境变量分别由argv_rc 和envp_rc 数组给出。参见后面的描述。
	if (!(pid=fork())) {
		// 子进程
		// 关闭标准输出，释放出文件id值0
		close(0);

		// 打开/etc/rc，这样就会将id值为0用作/etc/rc
		if (open("/etc/rc",O_RDONLY,0))
			_exit(1);

		// 执行/bin/sh，即shell。此时sh会从/etc/rc中读取文件内容
		// 然后执行完其中的命令后退出进程。
		execve("/bin/sh",argv_rc,envp_rc);	// argv_rc/envp_rc是给sh相应的环境变量和参数信息
		_exit(2);
	}
	if (pid>0)
		// 父进程在这里等待前面创建的子进程结束执行
		while (pid != wait(&i)) {}
			/* nothing */;
// --
// 如果执行到这里，说明刚创建的子进程的执行已停止或终止了。下面循环中首先再创建
// 一个子进程，如果出错，则显示“初始化程序创建子进程失败”的信息并继续执行。对
// 于所创建的子进程关闭所有以前还遗留的句柄(stdin, stdout, stderr)，新创建一个
// 会话并设置进程组号，然后重新打开/dev/tty0 作为stdin，并复制成stdout 和stderr。
// 再次执行系统解释程序/bin/sh。但这次执行所选用的参数和环境数组另选了一套（见上面）。
// 然后父进程再次运行wait()等待。如果子进程又停止了执行，则在标准输出上显示出错信息
//		“子进程pid 停止了运行，返回码是i”，
// 然后继续重试下去…，形成“大”死循环。
	// 再次创建子进程，以交互的方式执行shell
	// 下面是个循环，不断地启动shell进程
	while (1) {
		if ((pid=fork())<0) {
			printf("Fork failed in init\r\n");
			continue;
		}
		if (!pid) {
			// 关闭标准输入输出？？？为何要这样
			close(0);close(1);close(2);
			setsid();		// ???

			// 再次打开shell，此时传入的参数是-/bin/sh，带前缀-
			// shell的运行不同于上次，是以交互的方式运行
			(void) open("/dev/tty0",O_RDWR,0);
			(void) dup(0);
			(void) dup(0);
			_exit(execve("/bin/sh",argv,envp));
		}

		// 回收子进程和孤儿京城的任何资源
		while (1)
			// 不能让shell退出，如果确实这样了。那么就重启shell
			if (pid == wait(&i))
				break;
		printf("\n\rchild %d died with code %04x\n\r",pid,i);

		// 将所有数据回写磁盘，以够数据丢失
		// 在shell中可能执行了很多操作，有涉及到磁盘的，会缓存数据。回写安全一些
		sync();
	}

	// 普通的应用程序退出。。。一般不太可能执行到这里
	_exit(0);	/* NOTE! _exit, not exit() */
}
